/**
 * 
 */
package cz.cuni.mff.abacs.burglar.logics.planning;

import cz.cuni.mff.abacs.burglar.logics.GameMap;
import cz.cuni.mff.abacs.burglar.logics.objects.BaseObject;
import cz.cuni.mff.abacs.burglar.logics.objects.Room;
import cz.cuni.mff.abacs.burglar.logics.objects.agents.Agent;
import cz.cuni.mff.abacs.burglar.logics.objects.agents.BeliefBase;
import cz.cuni.mff.abacs.burglar.logics.objects.agents.Burglar;
import cz.cuni.mff.abacs.burglar.logics.objects.agents.Guard;
import cz.cuni.mff.abacs.burglar.logics.objects.items.Inventory;
import cz.cuni.mff.abacs.burglar.logics.objects.items.Item;
import cz.cuni.mff.abacs.burglar.logics.objects.positions.Camera;
import cz.cuni.mff.abacs.burglar.logics.objects.positions.Door;
import cz.cuni.mff.abacs.burglar.logics.objects.positions.Lockable;
import cz.cuni.mff.abacs.burglar.logics.objects.positions.Position;
import cz.cuni.mff.abacs.burglar.visual.VisualBurglar;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;


/**
 * Generates PDDL problem files.
 * 
 * Static class.
 * 
 * @author abacs
 *
 */
public class PDDLGenerator extends PDDLBase {
	
	
	// -------------------------------------------------------------------------
	// constructors:
	
	
	protected PDDLGenerator() {  }
	
	
	// -------------------------------------------------------------------------
	
	
	/**
	 * Generates a burglar problem.
	 * 
	 * The burglar object is looked up in the map file.
	 * 
	 * @param map the environment
	 * @param roomsToEnter rooms that needs to be included in the path
	 * @param roomsToAvoid rooms that needs to be avoided in the path
	 * @param fileName output filename
	 */
	public static void generateBurglarProblem(
			GameMap map,
			List<Integer> roomsToEnter,
			List<Integer> roomsToAvoid,
			String fileName
	) {
		String problem = 
			PDDLGenerator.generateBurglarProblem(map, roomsToEnter, roomsToAvoid);
		PDDLGenerator.writeToFile(problem, fileName);
		
		if(VisualBurglar.FLAG_SAVE_PLANNING_PROBLEMS){
			java.util.Date date = new java.util.Date();
			Timestamp tStamp = new Timestamp(date.getTime());
			PDDLGenerator.writeToFile(problem, fileName + "." + tStamp + ".pddl");
		}
		
	}
	
	
	/**
	 * Generates a burglar problem.
	 * 
	 * @param guard	the selected guard to plan for 
	 * @param map the environment
	 * @param roomsToEnter rooms that needs to be included in the path
	 * @param roomsToAvoid rooms that needs to be avoided in the path
	 * @param fileName output filename
	 */
	public static void generateGuardProblem(
			Guard guard,
			GameMap map,
			List<Integer> roomsToEnter,
			List<Integer> roomsToAvoid,
			String fileName
	) {
		String problem = 
				PDDLGenerator.generateGuardProblem(map, guard, roomsToEnter, roomsToAvoid);
		PDDLGenerator.writeToFile(problem, fileName);
		
		if(VisualBurglar.FLAG_SAVE_PLANNING_PROBLEMS){
			java.util.Date date = new java.util.Date();
			Timestamp tStamp = new Timestamp(date.getTime());
			PDDLGenerator.writeToFile(problem, fileName + "." + tStamp + ".pddl");
		}
	}
	
	
	// -------------------------------------------------------------------------
	
	
	/**
	 * Generates a string for the burglar.
	 * 
	 * The burglar object is looked up in the map file.
	 * 
	 * @param map the environment
	 * @param roomsToEnter rooms that needs to be included in the path
	 * @param roomsToAvoid rooms that needs to be avoided in the path
	 * @return string problem
	 */
	public static String generateBurglarProblem(
			GameMap map,
			List<Integer> roomsToEnter,
			List<Integer> roomsToAvoid
	) {
		if(VisualBurglar.FLAG_ALL_KNOWING_AGENTS)
			return generateAllKnowingAgentProblem(
					map, map.getBurglar(),
					roomsToEnter, roomsToAvoid
			);
		else
			return generateLimitedKnowledgeAgentProblem(
					map, map.getBurglar(),
					roomsToEnter, roomsToAvoid
			);
	}
	
	
	/**
	 * Generates a string for the guard.
	 * 
	 * @param guard	the selected guard to plan for 
	 * @param map the environment
	 * @param roomsToEnter rooms that needs to be included in the path
	 * @param roomsToAvoid rooms that needs to be avoided in the path
	 * @return string problem
	 */
	public static String generateGuardProblem(
			GameMap map,
			Guard guard,
			List<Integer> roomsToEnter,
			List<Integer> roomsToAvoid
	) {
		if(VisualBurglar.FLAG_ALL_KNOWING_AGENTS)
			return generateAllKnowingAgentProblem(
					map, guard,
					roomsToEnter, roomsToAvoid
			);
		else
			return generateLimitedKnowledgeAgentProblem(
					map, guard, 
					roomsToEnter, roomsToAvoid
			);
	}
	
	
	// -------------------------------------------------------------------------
	
	
	/** 
	 * Generates a string for the burglar based on it's beliefs and state.
	 * 
	 * @param map the environment
	 * @return string problem
	 */
	public static String generateDiscoveringBurglarProblem(GameMap map) {
		StringBuilder builder = new StringBuilder();
		
		Burglar burglar = map.getBurglar();
		
		// generate the header:
		builder.append("(define (problem burglar)\n");
		builder.append("\t(:domain burglar)\n");
		
		// ----------------------------
		// list the objects:
		builder.append(STR_TAG_OBJECTS);
		
		// sort positions by type:
		List<Position> positions = burglar.getBeliefBase().getKnownPositions();
		
		String strPositions = PDDLGenerator.generatePositionList(positions, true);
		builder.append(strPositions);
		
		// rooms:
		List<Room> rooms = map.getRooms();
		String strRooms = PDDLGenerator.generateRoomList(rooms);
		builder.append(strRooms);
		
		// items:
		List<Item> items = map.getItems();
		String strItems = PDDLGenerator.generateItemList(items);
		builder.append(strItems);
		
		// agents:
		List<Agent> agents = new ArrayList<Agent>();
		agents.add(burglar);
		String strAgents = PDDLGenerator.generateAgentList(agents);
		builder.append(strAgents);
		
		builder.append(STR_TAG_END);
		
		// ----------------------------
		// init:
		builder.append(STR_TAG_INIT);
		
		// init position details:
		for(Position pos : positions){
			if(pos.isTypeOf(BaseObject.Type.DOOR)){
				String posName = generateObjectName(pos);
				
				if(((Lockable)pos).isClosed())
					generateInlineTag(builder, STR_TAG_CLOSED, posName);
				if(((Lockable)pos).isLocked())
					generateInlineTag(builder, STR_TAG_LOCKED, posName);
				if(((Lockable)pos).getKeyId() > 0){
					generateInlineTag(
							builder,
							STR_TAG_NEEDS_KEY,
							posName,
							generateObjectName(((Lockable)pos).getKeyId())
					);
				}
				
				Room[] connectedRooms = ((Door)pos).getRooms();
				
				generateInlineTag(
						builder, 
						STR_TAG_DOOR_CONNECTS,
						posName,
						generateObjectName(connectedRooms[0]),
						generateObjectName(connectedRooms[1])
				);
				
				generateInlineTag(
						builder, 
						STR_TAG_DOOR_CONNECTS,
						posName,
						generateObjectName(connectedRooms[1]),
						generateObjectName(connectedRooms[0])
				);
				
				generateInlineTag(
						builder, 
						STR_TAG_POSITION_IN,
						posName,
						generateObjectName(connectedRooms[0])
				);
				
				generateInlineTag(
						builder, 
						STR_TAG_POSITION_IN,
						posName,
						generateObjectName(connectedRooms[1])
				);
			}
			
		}
		
		// init agent:
		
		generateInlineTag(
				builder, 
				STR_TAG_AGENT_NEAR,
				generateObjectName(burglar),
				generateDummyName(STR_TYPE_FLOOR)
		);
		
		generateInlineTag(
				builder, 
				STR_TAG_POSITION_IN,
				generateDummyName(STR_TYPE_FLOOR),
				generateObjectName(burglar.getRoom())
		);
		
		generateInlineTag(
				builder, 
				STR_TAG_AGENT_IN,
				generateObjectName(burglar),
				generateObjectName(burglar.getRoom())
		);
		
		generateInlineTag(
				builder, 
				STR_TAG_VISITED,
				generateObjectName(burglar),
				generateObjectName(burglar.getRoom())
		);
		
		for(Item item : burglar.getItems()){
			generateInlineTag(
					builder,
					STR_TAG_CONTAINS,
					generateObjectName(burglar),
					generateObjectName(item)
			);
		}
		
		// finish init:
		builder.append(STR_TAG_END);
		
		// ----------------------------
		// define goal:
		builder.append("\t(:goal\n\t\t(and\n");
		
		generateInlineTag(
				builder, 
				'\t' + STR_TAG_AGENT_IN,
				generateObjectName(burglar),
				generateObjectName(burglar.getAimId())
		);
		
		generateInlineTag(
				builder, 
				'\t' + STR_TAG_CONTAINS,
				generateObjectName(burglar),
				generateObjectName(burglar.getItemIdToCollect())
		);
		
		builder.append("\t\t)\n\t)\n");
		
		// finish:
		builder.append(STR_TAG_END_);
		return builder.toString();
	}
	
	
	/** 
	 * Generates a string for the agent based on the full knowledge of the map.
	 * 
	 * @param agent the selected agent
	 * @param map the environment
	 * @param roomsToEnter rooms that needs to be included in the path
	 * @param roomsToAvoid rooms that needs to be avoided in the path
	 * @return string problem
	 */
	public static String generateLimitedKnowledgeAgentProblem(
			GameMap map,
			Agent agent,
			List<Integer> roomsToEnter,
			List<Integer> roomsToAvoid
	) {
		BeliefBase belief = agent.getBeliefBase();
		
		// sort positions by type:
		List<Position> positions = belief.getKnownPositions();
		
		// rooms:
		List<Integer> rooms = map.getRoomIds();
		
		// items:
		List<Item> items = map.getItems();
		
		// TODO
		
		// passive agents:
		List<Agent> passiveAgents = belief.getPassiveAgents(); 
		
		return generateAgentProblem(
				positions,
				rooms,
				items,
				passiveAgents,
				agent,
				roomsToEnter,
				roomsToAvoid
		);
	}
	
	
	/** 
	 * Generates a string for the agent based on the full knowledge of the map.
	 * 
	 * @param agent the selected agent
	 * @param map the environment
	 * @param roomsToEnter rooms that needs to be included in the path
	 * @param roomsToAvoid rooms that needs to be avoided in the path
	 * @return string problem
	 */
	public static String generateAllKnowingAgentProblem(
			GameMap map,
			Agent agent,
			List<Integer> roomsToEnter,
			List<Integer> roomsToAvoid
	) {
		// positions:
		List<Position> positions = map.getOperablePositions();
		
		// rooms:
		List<Integer> rooms = map.getRoomIds();
				
		// items:
		List<Item> items = map.getItems();
		
		// passive agents:
		List<Agent> passiveAgents = map.getPassiveAgents(); 
		
		return generateAgentProblem(
				positions,
				rooms,
				items,
				passiveAgents,
				agent,
				roomsToEnter,
				roomsToAvoid
		);
	}
	
	
	// -------------------------------------------------------------------------
	
	
	/** 
	 * Generates a string for the agent based on the knowledge given
	 * in the parameters.
	 * 
	 * @param knownPositions list of known positions
	 * @param knownRooms list of rooms
	 * @param knownItems items on the map
	 * @param passiveAgents other agents on the map
	 * @param agent the selected agent
	 * @param roomsToEnter rooms that needs to be included in the path
	 * @param roomsToAvoid rooms that needs to be avoided in the path
	 * @return string problem
	 */
	public static String generateAgentProblem(
			List<Position> knownPositions,
			List<Integer> knownRooms,
			List<Item> knownItems,
			List<Agent> passiveAgents,
			Agent agent,
			List<Integer> roomsToEnter,
			List<Integer> roomsToAvoid
	) {
		StringBuilder builder = new StringBuilder();
		
		// generate the header section:
		builder.append("(define (problem agent)\n");
		builder.append("\t(:domain agent)\n");
		
		// ----------------------------
		// list the objects:
		builder.append(STR_TAG_OBJECTS);
		
		// add the current position to the position list only if it's an unimportant one,
		// otherwise it should be there
		if(agent.getPosition().isOperable() == false){
			knownPositions.add(agent.getPosition());
		}
		
		// sort positions by type:
		String strPositions = PDDLGenerator.generatePositionList(knownPositions, true);
		builder.append(strPositions);
		
		// rooms:
		String strRooms = PDDLGenerator.generateRoomListFromIds(knownRooms);
		builder.append(strRooms);
		
		// items:
		String strItems = PDDLGenerator.generateItemList(knownItems);
		builder.append(strItems);
		
		// passive agents:
		String strPassiveAgents = PDDLGenerator.generatePassiveAgentList(passiveAgents);
		builder.append(strPassiveAgents);
		
		// the agent:
		List<Agent> agents = new LinkedList<Agent>();
		agents.add(agent);
		String strAgents = PDDLGenerator.generateAgentList(agents);
		builder.append(strAgents);
		
		
		builder.append(STR_TAG_END);
		
		// ----------------------------
		// generate the init sections:
		builder.append(STR_TAG_INIT);
		
		// init position details:
		for(Position pos : knownPositions){
			String posName = generateObjectName(pos);
			
			// location:
			if(pos.isTypeOf(BaseObject.Type.DOOR) == false){
				generateInlineTag(
						builder,
						STR_TAG_POSITION_IN,
						posName,
						generateObjectName(pos.getRoomId())
				);
			}
			
			// set type:
			switch(pos.getType()){
			case CONTAINER:
				if(((Lockable)pos).isClosed())
					generateInlineTag(builder, STR_TAG_CLOSED, posName);
				if(((Lockable)pos).isLocked())
					generateInlineTag(builder, STR_TAG_LOCKED, posName);
				if(((Lockable)pos).getKeyId() > 0){
					generateInlineTag(
							builder,
							STR_TAG_NEEDS_KEY,
							posName,
							generateObjectName(((Lockable)pos).getKeyId())
					);
				}
				for(Item item : ((Inventory)pos).getItems()){
					generateInlineTag(
							builder,
							STR_TAG_CONTAINS,
							posName,
							generateObjectName(item)
					);
				}
				break;
			case DOOR:
				if(((Lockable)pos).isClosed())
					generateInlineTag(builder, STR_TAG_CLOSED, posName);
				if(((Lockable)pos).isLocked())
					generateInlineTag(builder, STR_TAG_LOCKED, posName);
				if(((Lockable)pos).getKeyId() > 0){
					generateInlineTag(
							builder,
							STR_TAG_NEEDS_KEY,
							posName,
							generateObjectName(((Lockable)pos).getKeyId())
					);
				}
				
				int[] connectedRooms = ((Door)pos).getRoomIds();
				
				generateInlineTag(
						builder, 
						STR_TAG_DOOR_CONNECTS,
						posName,
						generateObjectName(connectedRooms[0]),
						generateObjectName(connectedRooms[1])
				);
				
				generateInlineTag(
						builder, 
						STR_TAG_DOOR_CONNECTS,
						posName,
						generateObjectName(connectedRooms[1]),
						generateObjectName(connectedRooms[0])
				);
				
				generateInlineTag(
						builder, 
						STR_TAG_POSITION_IN,
						posName,
						generateObjectName(connectedRooms[0])
				);
				
				generateInlineTag(
						builder, 
						STR_TAG_POSITION_IN,
						posName,
						generateObjectName(connectedRooms[1])
				);
				break;
			case FLOOR:
				// not an important position
				break;
			case VENDER:
				// TODO
				break;
			case CAMERA:
				if(((Camera)pos).isActive()){
					generateInlineTag(
							builder, 
							STR_TAG_ROOM_OBSERVED,
							generateObjectName(pos.getRoomId())
					);
				}
				
				break;
			}
		}
		
		// the passive agents:
		for(Agent passiveAgent : passiveAgents){
			String posName = generateObjectName(passiveAgent.getId());
			
			// location:
			generateInlineTag(
					builder,
					STR_TAG_POSITION_IN,
					posName,
					generateObjectName(passiveAgent.getRoomId())
			);	 
		}
		
		
		// agent init:
		
		generateInlineTag(
				builder, 
				STR_TAG_AGENT_NEAR,
				generateObjectName(agent.getId()),
				generateObjectName(agent.getPositionId())
		);
		
		generateInlineTag(
				builder, 
				STR_TAG_POSITION_IN,
				generateObjectName(agent.getPositionId()),
				generateObjectName(agent.getRoomId())
		);
		
		generateInlineTag(
				builder, 
				STR_TAG_AGENT_IN,
				generateObjectName(agent.getId()),
				generateObjectName(agent.getRoomId())
		);
		
		for(Item item : agent.getItems()){
			generateInlineTag(
					builder,
					STR_TAG_CONTAINS,
					generateObjectName(agent.getId()),
					generateObjectName(item)
			);
		}
		
		builder.append(generateTypeSpecificInit(agent));
		
		// finish init:
		builder.append(STR_TAG_END);
		
		// ----------------------------
		// define goal:
		builder.append("\t(:goal\n\t\t(and\n");
		
		builder.append(generateTypeSpecificGoals(agent));
		
		// require entering rooms if needed:
		for(Integer room : roomsToEnter){
			generateInlineTag(
					builder, 
					'\t' + STR_TAG_VISITED,
					generateObjectName(agent.getId()),
					generateObjectName(room)
			);
		}
		
		// require avoiding rooms if needed:
		for(Integer room : roomsToAvoid){
			builder.append("\t\t");
			builder.append(STR_TAG_NOT);
			builder.append(STR_TAG_VISITED);
			builder.append(generateObjectName(agent.getId()));
			builder.append(' ');
			builder.append(generateObjectName(room));
			builder.append(STR_TAG_END__);
			builder.append(STR_TAG_END_);
		}
		
		builder.append("\t\t)\n\t)\n");
		
		// finish:
		builder.append(STR_TAG_END_);
		return builder.toString();
	}
	
	
	
	private static String generateTypeSpecificGoals(Agent agent) {
		StringBuilder builder = new StringBuilder();
		
		if(agent.getGoals().hasVenderToVisit()){
			generateInlineTag(
					builder, 
					'\t' + STR_TAG_POSITION_USED,
					generateObjectName(agent.getId()),
					generateObjectName(agent.getGoals().getVenderIdToVisit())
			);
		}else{
			
			switch(agent.getType()){
			case BURGLAR:
				Burglar burglar = (Burglar)agent;
				generateInlineTag(
						builder, 
						'\t' + STR_TAG_AGENT_IN,
						generateObjectName(burglar.getId()),
						generateObjectName(burglar.getAimId())
				);
				
				generateInlineTag(
						builder, 
						'\t' + STR_TAG_CONTAINS,
						generateObjectName(burglar),
						generateObjectName(burglar.getItemIdToCollect())
				);
			
				builder.append("\t\t");
				builder.append(STR_TAG_NOT);
				builder.append(STR_TAG_OBSERVED);
				builder.append(generateObjectName(agent.getId()));
				builder.append(STR_TAG_END__);
				builder.append(STR_TAG_END_);
			
				break;
			case GUARD:
				Guard guard = (Guard) agent;
				for(Integer room : guard.getAllGoalRooms()){
					generateInlineTag(
							builder, 
							'\t' + STR_TAG_VISITED,
							generateObjectName(agent.getId()),
							generateObjectName(room)
					);
					if(guard.getRoomId() == room){
						generateInlineTag(
								builder, 
								'\t' + STR_TAG_AGENT_IN,
								generateObjectName(agent.getId()),
								generateObjectName(room)
						);
					}
				
				}
			
				break;
			}
		}
		return builder.toString();
	}
	
	
	private static String generateTypeSpecificInit(Agent agent) {
		StringBuilder builder = new StringBuilder();
		
		switch(agent.getType()){
		case BURGLAR:
			
			break;
		case GUARD:
			
			break;
		}
		
		return builder.toString();
	}
	
	
	// -------------------------------------------------------------------------
	
	

	private static void generateInlineTag(
			StringBuilder builder,
			String tagName,
			String param
	) {
		builder.append(tagName);
		builder.append(param);
		builder.append(STR_TAG_END_);
	}
	
	

	private static void generateInlineTag(
			StringBuilder builder,
			String tagName,
			String param1,
			String param2
	) {
		builder.append(tagName);
		builder.append(param1);
		builder.append(' ');
		builder.append(param2);
		builder.append(STR_TAG_END_);
	}
	
	

	private static void generateInlineTag(
			StringBuilder builder,
			String tagName,
			String param1,
			String param2,
			String param3
	) {
		builder.append(tagName);
		builder.append(param1);
		builder.append(' ');
		builder.append(param2);
		builder.append(' ');
		builder.append(param3);
		builder.append(STR_TAG_END_);
	}
	
}
